/*
 *  Copyright 2011 derek.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.aocore.item;

import com.aocore.d2data.CharacterClass;
import com.aocore.d2data.EquipmentLocation;
import com.aocore.d2data.StatType;
import com.aocore.d2data.UnitType;
import com.aocore.util.BitReader;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author derek
 */
public class Item
{
    protected ItemAction action;
    protected ItemCategory category;
    protected int uid;

    // these are only ever set for 9d packet
    protected UnitType ownerType;
    protected int ownerUID;

    protected ItemFlags flags;
    protected ItemVersion version;
    protected ItemDestination destination;
    protected EquipmentLocation location;
    protected int x;
    protected int y;
    protected ItemContainer container = ItemContainer.Unspecified;

    protected String code;
    protected BaseItem baseItem;
    //protected BaseSetItem setItem = null;
    //protected BaseUniqueItem uniqueItem = null;
    //protected BaseRuneword runeword = null;

    protected ItemQuality quality = ItemQuality.NotApplicable;
    protected SuperiorItemType superiorType = SuperiorItemType.NotApplicable;
    protected ItemAffix prefix;
    protected ItemAffix suffix;
    protected List<MagicPrefixType> magicPrefixes;
    protected List<MagicSuffixType> magicSuffixes;

    protected String name = null;
    protected CharacterClass charClass = CharacterClass.NotApplicable;

    protected int level = -1;
    protected int usedSockets = -1;
    protected int use = -1;
    protected int graphic = -1;
    protected int color = -1;

    protected List<StatBase> stats = new ArrayList<StatBase>();
    protected List<StatBase> mods = null;
    protected List<StatBase>[] setBonuses = null;

    //UNKNOWN
    protected int unknown1 = -1;

    //DEBUG
    protected int runewordID = -1;
    protected int runewordParam = -1;

    /*
//tolua_begin
ItemActionType::ItemActionType action;
ItemCategory::ItemCategory category;
unsigned int uid;
// these are only ever set for 9d packet
UnitType::UnitType ownerType;
unsigned int ownerUID;
Flags flags;
ItemVersion::ItemVersion version;
ItemDestination::ItemDestination destination;
EquipmentLocation::EquipmentLocation location;
int x;
int y;
ItemContainer::ItemContainer container;

BaseItem baseItem;
BaseSetItem setItem;
BaseUniqueItem uniqueItem;
BaseRuneword runeword;

ItemQuality::ItemQuality quality;
SuperiorItemType::SuperiorItemType superiorType;
ItemAffix prefix;
ItemAffix suffix;
std::vector<MagicPrefixType::MagicPrefixType> magicPrefixes;
std::vector<MagicSuffixType::MagicSuffixType> magicSuffixes;

std::string name;
CharacterClass::CharacterClass charClass;

int level;
int usedSockets;
int use;
int graphic;
int color;

std::vector<StatBase> stats;
std::vector<StatBase> mods;
std::vector<StatBase> setBonuses[5];

//UNKNOWN
int unknown1;// = -1;

//DEBUG
int runewordID;// = -1;
int runewordParam;// = -1;

AreaLevel::AreaLevel area;
 */    
    
    
    public Item(byte[] data)
    {
        BitReader br = new BitReader(data, 1);

        this.action = ItemAction.valueOf(br.getByte());
        br.skipBytes(1); // length
        this.category = ItemCategory.valueOf(br.getByte());
        this.uid = br.getInt();

        if (data[0] == (byte)0x9d)
        {
            this.ownerType = UnitType.valueOf(br.getByte());
            this.ownerUID = br.getInt();
        }
        else
        {
            this.ownerType = UnitType.Invalid;
            this.ownerUID = 0;
        }

        int flags = br.getInt();
        this.version = ItemVersion.valueOf(br.getByte());

        //TODO: May be part of version... never seen those bits set.
        this.unknown1 = br.getByte(2);

        this.destination = ItemDestination.valueOf(br.getByte(3));

        if (this.destination == ItemDestination.Ground)
        {
            this.x = br.getShort();
            this.y = br.getShort();
        }
        else
        {
            this.location = EquipmentLocation.valueOf(br.getByte(4));
            this.x = br.getByte(4);
            this.y = br.getByte(3);
            this.container = ItemContainer.valueOf(br.getByte(4));
        }

        // Buffer to container mapping (sanitizes NPC tabs IDs and coords and changes belt location to X, Y)
        if (this.action == ItemAction.AddToShop || this.action == ItemAction.RemoveFromShop)
        {
            int buff = this.container.getId() | 0x80;
            if ((buff & 1) == 1)
            {
                buff--;
                this.y += 8;
            }
            this.container = ItemContainer.valueOf(buff);
        }
        else if (this.container == ItemContainer.Unspecified)
        {
            if (this.location == EquipmentLocation.NotApplicable)
            {
                if ((flags & ItemFlags.InSocket.getId()) == ItemFlags.InSocket.getId())//TODO
                //if ((flags & ItemFlags::InSocket) == ItemFlags::InSocket) 
                {
                    this.container = ItemContainer.Item;
                    this.y = -1;
                }
                else if (this.action == ItemAction.PutInBelt || this.action == ItemAction.RemoveFromBelt)
                {
                    this.container = ItemContainer.Belt;
                    this.y = this.x / 4;
                    this.x = this.x % 4;
                }
            }
            else
            {
                this.x = -1;
                this.y = -1;
            }
        }

        if ((flags & ItemFlags.Ear.getId()) == ItemFlags.Ear.getId())
        {
            this.charClass = CharacterClass.valueOf(br.getByte(3));
            this.level = br.getByte(7);
            this.name = br.getString(7, '\0', 16);
            //this.baseItem = BaseItem::Get(ItemType.Ear);
            return;
        }

        this.code = br.getString(' ');
        this.baseItem = BaseItem.get(code);

        // Big Pile : 1
        // Quantity : Big Pile ? 32 : 12
        if (this.code.equals("gld"))
        {
            int quantity = br.getInt(br.getBoolean(1) ? 32 : 12);
            return;
        }

        // Used Sockets : 3
        this.usedSockets = br.getByte(3);

        // Ends here if SimpleItem or Gamble
        if ((int)(flags & (ItemFlags.Compact.getId() | ItemFlags.Gamble.getId())) != 0)
        {
            return;
        }

        // ILevel : 7
        this.level = br.getByte(7);

        // Quality : 4
        this.quality = ItemQuality.valueOf(br.getByte(4));

        // Graphic : 1 : 3
        if (br.getBoolean(1))
            this.graphic = br.getByte(3);

        // Color : 1 : 11
        if (br.getBoolean(1))
            this.color = br.getInt(11);

        // Quality specific information
        if ((flags & ItemFlags.Identified.getId()) == ItemFlags.Identified.getId())
        {
            switch (this.quality)
            {
                case Inferior:
                {
                    byte prefixId = br.getByte(3);
                    System.out.println("prefixId: " + prefixId);
                    //this.prefix = ItemAffix(ItemAffixType::InferiorPrefix, br.ReadByte(3));
                }
                break;

                case Superior:
                {
                    short prefixId = br.getByte(3);
                    this.superiorType = SuperiorItemType.valueOf(br.getByte(3));
                    System.out.println("prefixId: " + prefixId + ", " + "superiorType: " + superiorType);
                    //this.prefix = ItemAffix(ItemAffixType::SuperiorPrefix, 0);
                }
                break;

                case Magic:
                {
                    short prefixId = br.getShort(11);
                    short suffixId = br.getShort(11);
                    System.out.println("prefixId: " + prefixId + ", " + "suffixId: " + suffixId);
                    //this.prefix = ItemAffix(ItemAffixType::MagicPrefix, br.ReadUInt16(11));
                    //this.suffix = ItemAffix(ItemAffixType::MagicSuffix, br.ReadUInt16(11));
                }
                    break;

                case Rare:
                case Crafted:
                {
                    byte prefixId = br.getByte(8);
                    byte suffixId = br.getByte(8);
                    System.out.println("prefixId: " + prefixId + ", " + "suffixId: " + suffixId);
                    //this.prefix = ItemAffix(ItemAffixType::RarePrefix, br.ReadByte(8));
                    //this.suffix = ItemAffix(ItemAffixType::RareSuffix, br.ReadByte(8));
                }
                break;

                case Set:
                {
                    short setId = br.getShort(12);
                    System.out.println("setId: " + setId);
                    //this.setItem = BaseSetItem::Get(br.ReadUInt16(12));
                }
                break;

                case Unique:
                {
                    if (!code.equals("std") && !code.equals("hdm") && !code.equals("te1") && !code.equals("te2") && !code.equals("te3") && !code.equals("te4")) // TODO: add UniqueItem entry to parse mod (req lvl 90)
                    {
                        short uniqueId = br.getShort(12);
                        System.out.println("uniqueId: " + uniqueId);
                        //this.uniqueItem = BaseUniqueItem::Get(br.ReadUInt16(12));
                    }
                }
                break;
            }
        }

        if (this.quality == ItemQuality.Rare || this.quality == ItemQuality.Crafted)
        {
            this.magicPrefixes = new ArrayList<MagicPrefixType>();
            this.magicSuffixes = new ArrayList<MagicSuffixType>();

            for (int i = 0; i < 3; i++)
            {
                if (br.getBoolean(1))
                {
                    this.magicPrefixes.add(MagicPrefixType.valueOf(br.getShort(11)));
                }

                if (br.getBoolean(1))
                {
                    this.magicSuffixes.add(MagicSuffixType.valueOf(br.getShort(11)));
                }
            }
        }

        // Runeword Info : 16
        if ((flags & ItemFlags.Runeword.getId()) == ItemFlags.Runeword.getId())
        {
            //HACK: this is probably very wrong, but works for all the runewords I tested so far...
            //TODO: remove these fields once testing is done
            this.runewordID = br.getShort(12);
            this.runewordParam = br.getShort(4);

            int val = -1;
            if (this.runewordParam == 5) //TODO: Test cases where ID is around 100...
            {
                val = this.runewordID - this.runewordParam * 5;
                if (val < 100) val--;
            }
            else if (this.runewordParam == 2) //TODO: Test other runewords than Delirium...
            {
                val = ((this.runewordID & 0x3FF) >> 5) + 2;
            }

            //TODO: Test other runewords, find real shift / add params...
            br.rewindBytes(2);
            this.runewordParam = br.getShort();
            this.runewordID = val;

//            if (val != -1)
  //              this.runeword = BaseRuneword::Get(val);
    //        else
      //          throw "Unknown Runeword";
        }

        // Personalized Name : 7 * (NULLSTRING Length)
        if ((flags & ItemFlags.Personalized.getId()) == ItemFlags.Personalized.getId())
        {
            this.name = br.getString(7, '\0', 16);
        }

        if (this.category.isArmor())
        {
            int defense = br.getInt(11) - 10;
            System.out.println("defense: " + defense);
        }

        // Even items marked NoDurability have a durability, it just normally isn't used (e.g. bows...)
        if (this.category.isArmor() || this.category.isWeapon())
        {
            //baseStat = BaseStat::Get(StatType::MaxDurability);
            int maxDurability = br.getInt(8);
            //this.stats.push_back(SignedStat(baseStat, val));

            // 0 max durability means indestructible and that there's no current durability.
            // This is only found on old items subject to the "zod bug"...
            if (maxDurability > 0)
            {
                //baseStat = BaseStat::Get(StatType::Durability);
                //this.stats.push_back(SignedStat(baseStat, br.ReadInt32(baseStat->SaveBits)));
                int durability = br.getInt(8);
                System.out.println("maxDurability: " + maxDurability + ", durability: " + durability);
            }
            else
            {
                System.out.println("maxDurability: " + maxDurability + ", durability: indestructible");
            }
        }

        if ((flags & ItemFlags.Socketed.getId()) == ItemFlags.Socketed.getId())
        {
            int sockets = br.getInt(4);
            System.out.println("sockets: " + sockets);
            //baseStat = BaseStat::Get(StatType::Sockets);
            //item.stats.push_back(SignedStat(baseStat, br.ReadInt32(baseStat->SaveBits)));
        }

        // Useable : 5; Quantity : 9
        if (this.baseItem.isStackable())
        {
            if (this.baseItem.isUseable())
                this.use = br.getByte(5);

            int quantity = br.getInt(9);
            //this.stats.push_back(SignedStat(BaseStat::Get(StatType::Quantity), br.ReadInt32(9)));
        }

        if ((flags & ItemFlags.Identified.getId()) == ItemFlags.Identified.getId())
        {
            return;
        }

        // Set Bonus Stats
        int setMods = this.quality == ItemQuality.Set ? br.getByte(5) : -1;

        StatBase stat;
        //item.mods = new List<StatBase>();

        if (code.equals("std") && code.equals("hdm") && code.equals("te1") && code.equals("te2") && code.equals("te3") && code.equals("te4"))
        {
            while ((stat = ReadStat(br)) != null)
            {
                this.mods.add(stat);
            }

            if ((flags & ItemFlags.Runeword.getId()) == ItemFlags.Runeword.getId())
            {
                while ((stat = ReadStat(br)) != null)
                {
                    this.mods.add(stat);
                }
            }

            if (setMods > 0)
            {
                //item.setBonuses = new std::list<StatBase>[5];
                for (int i = 0; i < 5; i++)
                {
                    if ((setMods & (1 << i)) != 0)
                    {
                        //item.setBonuses[i] = new std::list<StatBase>();
                        while ((stat = ReadStat(br)) != null)
                        {
                            this.setBonuses[i].add(stat);
                        }
                    }
                }
            }
        }
    }

    static StatBase ReadStat(BitReader br)
    {
        int statID = br.getInt(9);

        if (statID == 0x1ff)
        {
            return null;
        }

        BaseStat baseStat = BaseStat.get(statID);

        if (baseStat.getSaveParamBits() != -1)
        {
            switch (baseStat.getType())
            {
                // Multiple param stats
                case Reanimate:
                {
                    int monster = br.getInt(baseStat.getSaveParamBits());
                    int val = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - monster: " + monster + ", val: " + val);
                    //return new ReanimateStat(baseStat, monster, val);
                }
                case ElementalSkillBonus:
                {
                    int element = br.getInt(baseStat.getSaveParamBits());
                    int val = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - element: " + element + ", val: " + val);
                    //return new ElementalSkillsBonusStat(baseStat, element, val);
                }
                case ClassSkillsBonus:
                {
                    int charClass = br.getInt(baseStat.getSaveParamBits());
                    int val = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - charClass: " + charClass + ", val: " + val);
                    //return new ClassSkillsBonusStat(baseStat, charClass, val);
                }
                case Aura:
                {
                    int skill = br.getInt(baseStat.getSaveParamBits());
                    int val = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - skill: " + skill + ", val: " + val);
                    //return new AuraStat(baseStat, skill, val);
                }
                case SingleSkill:
                case NonClassSkill:
                {
                    int skill = br.getInt(baseStat.getSaveParamBits());
                    int val = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - skill: " + skill + ", val: " + val);
                    //return new SkillBonusStat(baseStat, skill, val);
                }
                case ChargedSkill:
                {
                    int level = br.getInt(6);
                    int skill = br.getInt(10);
                    int charges = br.getInt(8);
                    int maxCharges = br.getInt(8);

                    System.out.println(baseStat.getType() + " - level: " + level + ", skill: " + skill + ", charges: " + charges + ", maxCharges: " + maxCharges);
                    //return new ChargedSkillStat(baseStat, level, skill, charges, maxCharges);
                }
                case SkillOnAttack:
                case SkillOnKill:
                case SkillOnDeath:
                case SkillOnStriking:
                case SkillOnLevelUp:
                case SkillOnGetHit:
                {
                    int level = br.getInt(6);
                    int skill = br.getInt(10);
                    int chance = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - level: " + level + ", skill: " + skill + ", chance: " + chance);
                    //return new SkillOnEventStat(baseStat, level, skill, chance);
                }
                case SkillTabBonus: 
                {
                    int tab = br.getInt(3);
                    int charClass = br.getInt(3);
                    int unknown = br.getInt(10);
                    int val = br.getInt(baseStat.getSaveBits());

                    System.out.println(baseStat.getType() + " - tab: " + tab + ", charClass: " + charClass + ", unknown: " + unknown + ", val: " + val);
                    //return new SkillTabBonusStat(baseStat, tab, charClass, unknown, val);
                }
                default:
                    // Shouldn't get here...
//                    throw new Exception("StatBase ReadStat(BitReader br)");
            }
        }

        // Per level stats: val / (1 << stat.OpParam) per Level
        if (baseStat.getOpBase() == StatType.Level)
        {
            //return new PerLevelStat(baseStat, br.ReadInt32(baseStat->SaveBits));
        }

        switch (baseStat.getType())
        {
            // Range stats:
            case MinDamagePercent:
            case MaxDamagePercent:
            {
                int min = br.getInt(baseStat.getSaveBits());
                int max = br.getInt(baseStat.getSaveBits());

                System.out.println(baseStat.getType() + " - min: " + min + ", max: " + max);
                //return new DamageRangeStat(baseStat, min, max);
            }
            case FireMinDamage:
            case LightMinDamage:
            case MagicMinDamage:
            {
                int min = br.getInt(baseStat.getSaveBits());
                //int max = br.getInt(BaseStat::Get(baseStat->Index + 1)->SaveBits);

                System.out.println(baseStat.getType() + " - min: " + min/* + ", max: " + max*/);
                //return new DamageRangeStat(baseStat, min, max);
            }
            case ColdMinDamage:
            {
                int min = br.getInt(baseStat.getSaveBits());
//                int max = br.getInt(BaseStat.get(StatType.ColdMaxDamage).getSaveBits());
//                int frames = br.getInt(BaseStat.get(StatType.ColdLength).getSaveBits());

                System.out.println(baseStat.getType() + " - min: " + min/* + ", max: " + max + ", frames: " + frames*/);
                //return new ColdDamageStat(baseStat, min, max, frames);
            }
            case PoisonMinDamage:
            {
                int min = br.getInt(baseStat.getSaveBits());
//                int max = br.getInt(BaseStat.get(StatType.PoisonMaxDamage).getSaveBits());
//                int frames = br.getInt(BaseStat.get(StatType.PoisonLength).getSaveBits());

                System.out.println(baseStat.getType() + " - min: " + min/* + ", max: " + max + ", frames: " + frames*/);
                //return new PoisonDamageStat(baseStat, min, max, frames);
            }

            // Single param stats:
            case ReplenishDurability:
            case ReplenishQuantity:
            {
                int val = br.getInt(baseStat.getSaveBits());

                System.out.println(baseStat.getType() + " - val: " + val);
                //return new ReplenishStat(baseStat, val);
            }

            default:
            {
                if (baseStat.isSigned())
                {
                    int val = br.getInt(baseStat.getSaveBits());

//                    if (baseStat->SaveAdd > 0)
//                    {
//                        val -= baseStat->SaveAdd;
//                    }

                    System.out.println(baseStat.getType() + " - signed val: " + val);
                    //return new SignedStat(baseStat, val);
                }
                else
                {
                    int val = br.getInt(baseStat.getSaveBits());

//                    if (baseStat->SaveAdd > 0)
//                    {
//                        val -= (unsigned int)baseStat->SaveAdd;
//                    }

                    System.out.println(baseStat.getType() + " - unsigned val: " + val);
                    //return new UnsignedStat(baseStat, val);
                }
            }
        }

        return null;
    }
}
